/*
 * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
 * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

using System;
using System.IO;
using QuantConnect.Util;
using System.Collections.Generic;
using QuantConnect.Securities.Future;
using System.Runtime.CompilerServices;

namespace QuantConnect.Data.UniverseSelection
{
    /// <summary>
    /// Represents a universe of futures data
    /// </summary>
    public class FutureUniverse : BaseChainUniverseData
    {
        /// <summary>
        /// Cache for the symbols to avoid creating them multiple times
        /// </summary>
        /// <remarks>Key: securityType, market, ticker, expiry</remarks>
        private static readonly Dictionary<(SecurityType, string, string, DateTime), Symbol> _symbolsCache = new();

        /// <summary>
        /// Creates a new instance of the <see cref="FutureUniverse"/> class
        /// </summary>
        public FutureUniverse()
        {
        }

        /// <summary>
        /// Creates a new instance of the <see cref="FutureUniverse"/> class
        /// </summary>
        public FutureUniverse(DateTime date, Symbol symbol, string csv)
            : base(date, symbol, csv)
        {
        }

        /// <summary>
        /// Creates a new instance of the <see cref="FutureUniverse"/> class as a copy of the given instance
        /// </summary>
        public FutureUniverse(FutureUniverse other)
            : base(other)
        {
        }

        /// <summary>
        /// Reader converts each line of the data source into BaseData objects. Each data type creates its own factory method, and returns a new instance of the object
        /// each time it is called.
        /// </summary>
        /// <param name="config">Subscription data config setup object</param>
        /// <param name="stream">Stream reader of the source document</param>
        /// <param name="date">Date of the requested data</param>
        /// <param name="isLiveMode">true if we're in live mode, false for backtesting mode</param>
        /// <returns>Instance of the T:BaseData object generated by this line of the CSV</returns>
        [StubsIgnore]
        public override BaseData Reader(SubscriptionDataConfig config, StreamReader stream, DateTime date, bool isLiveMode)
        {
            if (stream == null || stream.EndOfStream)
            {
                return null;
            }

            if (stream.Peek() == '#')
            {
                // Skip header
                stream.ReadLine();
                return null;
            }

            var futureContractMonth = stream.GetDateTime(DateFormat.YearMonth);
            var cacheKey = (config.SecurityType, config.Market, config.Symbol.ID.Symbol, futureContractMonth);
            if (!TryGetCachedSymbol(cacheKey, out var symbol))
            {
                var expiry = FuturesExpiryUtilityFunctions.GetFutureExpirationFromContractMonth(config.Symbol, futureContractMonth);
                symbol = Symbol.CreateFuture(config.Symbol.ID.Symbol, config.Symbol.ID.Market, expiry);
                CacheSymbol(cacheKey, symbol);
            }

            return new FutureUniverse(date, symbol, stream.ReadLine());
        }

        /// <summary>
        /// Creates a copy of the instance
        /// </summary>
        /// <returns>Clone of the instance</returns>
        public override BaseData Clone()
        {
            return new FutureUniverse(this);
        }

        /// <summary>
        /// Gets the default resolution for this data and security type
        /// </summary>
        /// <remarks>This is a method and not a property so that python
        /// custom data types can override it</remarks>
        public override Resolution DefaultResolution()
        {
            return Resolution.Daily;
        }

        /// <summary>
        /// Implicit conversion into <see cref="Symbol"/>
        /// </summary>
        /// <param name="data">The option universe data to be converted</param>
#pragma warning disable CA2225 // Operator overloads have named alternates
        public static implicit operator Symbol(FutureUniverse data)
#pragma warning restore CA2225 // Operator overloads have named alternates
        {
            return data.Symbol;
        }

        /// <summary>
        /// Gets the CSV string representation of this universe entry
        /// </summary>
        public static string ToCsv(Symbol symbol, decimal open, decimal high, decimal low, decimal close, decimal volume, decimal? openInterest)
        {
            var contractMonth = FuturesExpiryUtilityFunctions.GetFutureContractMonth(symbol);
            return $"{contractMonth.ToString(DateFormat.YearMonth)},{open},{high},{low},{close},{volume},{openInterest}";
        }

        /// <summary>
        /// Gets the CSV header string for this universe entry
        /// </summary>
        public static string CsvHeader => "expiry,open,high,low,close,volume,open_interest";

        /// <summary>
        /// Tries to get a symbol from the cache
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        protected static bool TryGetCachedSymbol((SecurityType, string, string, DateTime) key, out Symbol symbol)
        {
            lock (_symbolsCache)
            {
                return _symbolsCache.TryGetValue(key, out symbol);
            }
        }

        /// <summary>
        /// Caches a symbol
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        protected static void CacheSymbol((SecurityType, string, string, DateTime) key, Symbol symbol)
        {
            lock (_symbolsCache)
            {
                // limit the cache size to help with memory usage
                if (_symbolsCache.Count >= 100000)
                {
                    _symbolsCache.Clear();
                }
                _symbolsCache.TryAdd(key, symbol);
            }
        }
    }
}
